<template>
  <div>
    <el-form ref="form" :model="bo" size="mini">
      <el-collapse v-model="activeNames">
        <CollapseItem title="General">
          <template #content>
            <FormItemInput
              v-if="propertyVisible('id')"
              v-model="bo.id"
              label="Id"
              :rules="[
                { message: $customTranslate('Element must have an unique id.'), trigger: 'blur', required: true },
                { message: $customTranslate('Must have max length {length}', { length: 5 }), trigger: 'blur', max: 255 }
              ]"
              prop="id"
            />
            <FormItemTextArea
              v-if="propertyVisible('name')"
              v-model="bo.name"
              label="Name"
              :rules="[{ message: $customTranslate('Must have max length {length}', { length: 255 }), trigger: 'blur', max: 255 }]"
              prop="name"
            />
            <slot name="general" />
          </template>
        </CollapseItem>
        <CollapseItem title="Documentation">
          <FormItemTextArea
            v-if="propertyVisible('documentation')"
            slot="content"
            v-model="doc"
            :placeholder="$customTranslate('Element Documentation')"
            :rules="[{ message: $customTranslate('Must have max length {length}', { length: 4000 }), trigger: 'blur', max: 4000 }]"
            prop="doc"
          />
        </CollapseItem>
        <CollapseItem v-if="templates.length" title="Template">
          <template #content>
            <el-form-item>
              <el-select v-model="selectedTemplate" value-key="id" filterable clearable>
                <el-option
                  v-for="(item, index) in templates"
                  :key="index"
                  :label="$customTranslate(item.name)"
                  :value="item"
                />
              </el-select>
            </el-form-item>
            <template v-if="selectedTemplate">
              <el-form-item v-if="selectedTemplate.description">
                <span style="color: #8492a6;">{{ $customTranslate(selectedTemplate.description) }}</span>
              </el-form-item>
              <template v-for="(item, index) in bo.templateProperties">
                <component
                  :is="propertyComponents[item.type]"
                  v-if="item.type !== 'Hidden'"
                  :key="index"
                  v-model="item.value"
                  :options="item.choices"
                  :label="item.label"
                  :disabled="item.editable === false"
                  :rules="convertToRules(item['constraints'])"
                  :prop="'templateProperties.' + index + '.value'"
                  :placeholder="item.description"
                  @change="change($event, item)"
                />
              </template>
            </template>
          </template>
        </CollapseItem>
        <slot name="custom" />
        <CollapseItem v-if="executionListenerVisible" title="Execution listeners">
          <el-form-item slot="content">
            <el-badge :value="listenerLength">
              <el-button @click="showListener = true">
                {{ $customTranslate('Update') }}
              </el-button>
            </el-badge>
          </el-form-item>
        </CollapseItem>
        <CollapseItem v-if="propertiesVisible" title="Extension properties">
          <el-form-item slot="content">
            <el-badge :value="properties.length">
              <el-button @click="showProperty = true">
                {{ $customTranslate('Update') }}
              </el-button>
            </el-badge>
          </el-form-item>
        </CollapseItem>
      </el-collapse>
    </el-form>
    <ExecutionListener
      v-if="showListener && executionListenerVisible"
      :moddle="moddle"
      :bo="bo"
      @write="write"
      @close="finishListener"
    />
    <Property
      v-if="showProperty && propertiesVisible"
      v-model="properties"
      :moddle="moddle"
      :is-form="false"
      @save-properties="writeProperties"
      @close="showProperty = false"
    />
  </div>
</template>
<script>
import ExecutionListener from '../part/listener/ExecutionListener'
import Property from '../part/detail/Property'
import CollapseItem from '../ui/CollapseItem'
import FormItemInput from '../ui/FormItemInput'
import FormItemSelect from '../ui/FormItemSelect'
import FormItemSwitch from '../ui/FormItemSwitch'
import FormItemTextArea from '../ui/FormItemTextArea'
import areaHelper from '../../mixins/areaHelper'
import { is } from 'bpmn-js/lib/util/ModelUtil'
import { customize, findOutputParameter, isInOut, isInOutVariable } from '../../utils'
import {
  addAndRemoveElementsFromExtensionElements,
  createCamundaIn,
  createCamundaInWithBusinessKey,
  createCamundaOut,
  createCamundaFieldInjection,
  createOutputParameter,
  createInputParameter,
  createCamundaProperty,
  createFormalExpression,
  createExtensionElements,
  createCamundaExecutionListenerScript
} from '../../utils/creators'
import { splitColon } from '../../utils/tools'
import store from '../../store'
import eventBus, { ExtensionElements_Changed } from '../../utils/eventBus'

const
  PROPERTY_TYPE = 'property',
  CAMUNDA_PROPERTY_TYPE = 'camunda:property',
  CAMUNDA_INPUT_PARAMETER_TYPE = 'camunda:inputParameter',
  CAMUNDA_OUTPUT_PARAMETER_TYPE = 'camunda:outputParameter',
  CAMUNDA_IN_TYPE = 'camunda:in',
  CAMUNDA_OUT_TYPE = 'camunda:out',
  CAMUNDA_IN_BUSINESS_KEY_TYPE = 'camunda:in:businessKey',
  CAMUNDA_EXECUTION_LISTENER_TYPE = 'camunda:executionListener',
  CAMUNDA_FIELD = 'camunda:field',
  // CAMUNDA_ERROR_EVENT_DEFINITION = 'camunda:errorEventDefinition',
  IO_BINDING_TYPES = [
    CAMUNDA_INPUT_PARAMETER_TYPE,
    CAMUNDA_OUTPUT_PARAMETER_TYPE
  ],
  IN_OUT_BINDING_TYPES = [
    CAMUNDA_IN_TYPE,
    CAMUNDA_OUT_TYPE,
    CAMUNDA_IN_BUSINESS_KEY_TYPE
  ]

export default {
  name: 'Base',
  components: {
    ExecutionListener,
    Property,
    CollapseItem,
    FormItemInput,
    FormItemSelect,
    FormItemSwitch,
    FormItemTextArea
  },
  mixins: [areaHelper],
  data() {
    return {
      doc: undefined,
      propertyComponents: {
        'String': 'FormItemInput',
        'Text': 'FormItemTextArea',
        'Dropdown': 'FormItemSelect',
        'Boolean': 'FormItemSwitch'
      },
      templates: store.getters.getTemplates(this.bo?.$type),
      listenerLength: 0,
      showListener: false,
      showProperty: false,
      properties: [],
      selectedTemplate: undefined,
      activeNames: ['general']
    }
  },
  computed: {
    executionListenerVisible() {
      return this.propertyVisible('executionListener')
    },
    propertiesVisible() {
      return this.propertyVisible('properties')
    }
  },
  watch: {
    'bo.id'(val) {
      this.setTitle()
      this.write({ id: val })
    },
    'bo.name'(val) {
      this.setTitle()
      this.write({ name: val })
    },
    'doc'(val) {
      this.write({
        documentation: val ? [this.moddle.create('bpmn:Documentation', { text: val })] : []
      })
    },
    selectedTemplate(newVal, oldVal) {
      if (oldVal === newVal) return
      const updateProperties = {}
      this.bo.templateProperties?.forEach(property => {
        this.handleProperty(updateProperties, property)
      })
      this.bo.templateProperties = []
      if (this.selectedTemplate?.properties) {
        this.selectedTemplate.properties.forEach(item => {
          const property = {
            ...item,
            value: this.readProperty(updateProperties, item) || item.value // 从现有bo中同步值
          }
          this.handleProperty(updateProperties, item, property.value)
          this.bo.templateProperties.push(property)
        })
      }
      updateProperties.modelerTemplate = this.bo.modelerTemplate = this.selectedTemplate?.id
      this.write(updateProperties)
      this.refresh()
    }
  },
  created() {
    this.init()
    this.load()
  },
  beforeDestroy() {
    eventBus.$off(ExtensionElements_Changed)
  },
  methods: {
    init() {
      if (this.bo.documentation?.length) {
        this.doc = this.bo.documentation[0].text
      }
      this.properties =
        this.bo.extensionElements?.values?.find(item => is(item, customize('Properties')))?.values ?? []
      this.computeLength()
    },
    finishListener() {
      this.computeLength()
      this.showListener = false
    },
    computeLength() {
      this.listenerLength = this.bo.extensionElements?.values
        ?.filter(item => is(item, customize('ExecutionListener'))).length ?? 0
    },
    writeProperties(propertiesElement) {
      this.showProperty = false
      this.properties = propertiesElement?.values || []
      const
        matcher = item => !is(item, customize('Properties')),
        objectsToAdd = propertiesElement ? [propertiesElement] : undefined
      this.write({ extensionElements:
          this.bo.extensionElements = addAndRemoveElementsFromExtensionElements(this.moddle, this.bo, objectsToAdd, matcher)
      })
    },
    setTitle() {
      store.commit('SET_NODE_TITLE', this.bo.name || this.bo.id)
    },
    load() {
      if (this.bo.modelerTemplate) {
        this.selectedTemplate = this.templates.find(template => template.id === this.bo.modelerTemplate)
      } else {
        this.selectedTemplate = this.templates.find(template => template.isDefault)
      }
      // 当extensionElements发生变更时，刷新模板的配置项
      eventBus.$on(ExtensionElements_Changed, () => {
        const updateProperties = {}
        this.bo.templateProperties?.forEach(property => {
          property.value = this.readProperty(updateProperties, property) || property.value
        })
      })
    },
    readProperty(updateProperties, property) {
      const binding = property['binding'],
        bindingType = binding.type
      let value, values
      if (bindingType === PROPERTY_TYPE) {
        const bindingName = splitColon(binding.name)
        if (!(bindingName in updateProperties)) { // 不在updateProperties中的，才会读现在的bo
          if (bindingName === 'conditionExpression') {
            value = this.bo.conditionExpression?.body
          } else {
            value = this.bo[bindingName]
          }
        }
      } else if ((values = this.bo.extensionElements?.values)?.length) {
        if (bindingType === CAMUNDA_PROPERTY_TYPE) {
          const propertiesElement = values.find(item => is(item, customize('Properties')))
          if (propertiesElement?.values) {
            const propertyElement = propertiesElement.values?.find(item => item.name === binding.name)
            if (propertyElement) {
              value = propertyElement.value
            }
          }
        } else if (IO_BINDING_TYPES.indexOf(bindingType) !== -1) {
          const ioElement = values.find(item => is(item, customize('InputOutput')))
          if (ioElement) {
            if (bindingType === CAMUNDA_INPUT_PARAMETER_TYPE) {
              const inputParameterElement = ioElement.get('inputParameters')?.find(item => item.name === binding.name)
              if (inputParameterElement) {
                value = (inputParameterElement.definition || inputParameterElement).value
              }
            }
            if (bindingType === CAMUNDA_OUTPUT_PARAMETER_TYPE) {
              const outputParameterElement = findOutputParameter(ioElement, binding)
              if (outputParameterElement) {
                value = outputParameterElement.name
              }
            }
          }
        } else if (IN_OUT_BINDING_TYPES.indexOf(bindingType) !== -1) {
          let matcher, ioElement
          if (bindingType === CAMUNDA_IN_TYPE) {
            matcher = item => is(item, customize('In')) && isInOut(item, binding)
            if ((ioElement = values.find(matcher)) !== undefined) {
              value = 'sourceExpression' in property ? ioElement.sourceExpression : ioElement.source
            }
          } else if (bindingType === CAMUNDA_OUT_TYPE) {
            matcher = item => is(item, customize('Out')) && isInOut(item, binding)
            if ((ioElement = values.find(matcher)) !== undefined) {
              value = ioElement.target
            }
          } else {
            matcher = item => is(item, customize('In')) && 'businessKey' in item
            if ((ioElement = values.find(matcher)) !== undefined) {
              value = ioElement.businessKey
            }
          }
        }
      }
      return value
    },
    handleProperty(updateProperties, property, value) {
      const binding = property['binding'],
        bindingType = binding.type,
        extensionElements = createExtensionElements(this.moddle, updateProperties)
      if (bindingType === PROPERTY_TYPE) {
        let propertyValue
        if (binding.name === 'conditionExpression') {
          propertyValue = createFormalExpression(this.moddle, {
            body: value,
            language: binding.scriptFormat
          })
        } else {
          propertyValue = value
        }
        if (propertyValue || binding.name !== 'id') { // id是必填项，需要做特殊处理
          updateProperties[splitColon(binding.name)] = propertyValue
        }
      } else if (bindingType === CAMUNDA_PROPERTY_TYPE) {
        let propertiesElement = extensionElements.values?.find(item => is(item, customize('Properties')))
        if (!propertiesElement) {
          propertiesElement = this.moddle.create(customize('Properties'))
          propertiesElement.values = []
          extensionElements.values.push(propertiesElement)
        }
        const matcher = item => item.name !== binding.name
        this.resolveList(propertiesElement, 'values', value, createCamundaProperty, binding, matcher)
        if (!propertiesElement.values.length) {
          extensionElements.values = extensionElements.values.filter(item => !is(item, customize('Properties')))
        }
      } else if (IO_BINDING_TYPES.indexOf(bindingType) !== -1) {
        let ioElement = extensionElements.values?.find(item => is(item, customize('InputOutput')))
        if (!ioElement) {
          ioElement = this.moddle.create(customize('InputOutput'))
          extensionElements.values.push(ioElement)
        }
        let matcher, propertyName, func
        if (bindingType === CAMUNDA_INPUT_PARAMETER_TYPE) {
          matcher = item => item.name !== binding.name
          propertyName = 'inputParameters'
          func = createInputParameter
        } else {
          matcher = item => item.name !== value
          propertyName = 'outputParameters'
          func = createOutputParameter
        }
        this.resolveList(ioElement, propertyName, value, func, binding, matcher)
        if (!ioElement['inputParameters']?.length && !ioElement['outputParameters']?.length) {
          extensionElements.values = extensionElements.values.filter(item => !is(item, customize('InputOutput')))
        }
      } else if (IN_OUT_BINDING_TYPES.indexOf(bindingType) !== -1) {
        let func, matcher
        if (bindingType === CAMUNDA_IN_TYPE) {
          func = createCamundaIn
          matcher = item => !(is(item, customize('In')) && isInOut(item, binding))
        } else if (bindingType === CAMUNDA_OUT_TYPE) {
          func = createCamundaOut
          matcher = item => !(is(item, customize('Out')) && isInOut(item, binding))
        } else {
          func = createCamundaInWithBusinessKey
          matcher = item => !(is(item, customize('In')) && 'businessKey' in item)
        }
        this.resolveList(extensionElements, 'values', value, func, binding, matcher)
      } else if (bindingType === CAMUNDA_FIELD) {
        const matcher = item => !(is(item, customize('Field')) && item.name === binding.name)
        this.resolveList(extensionElements, 'values', value, createCamundaFieldInjection, binding, matcher)
      } else if (bindingType === CAMUNDA_EXECUTION_LISTENER_TYPE) {
        const matcher = item => !is(item, customize('ExecutionListener') && item.event !== binding.event)
        this.resolveList(extensionElements, 'values', value, createCamundaExecutionListenerScript, binding, matcher)
      }
      updateProperties.extensionElements = extensionElements.values.length ? extensionElements : undefined
    },
    resolveList(bo, propertyName, value, createFunction, binding, matcher) {
      bo[propertyName] = bo[propertyName]?.filter(matcher) ?? [] // delete
      if (value || isInOutVariable(binding)) { // create & update
        bo[propertyName].push(createFunction(this.moddle, binding, value))
      }
    },
    change(value, property) {
      const updateProperties = { extensionElements: this.bo.extensionElements }
      this.handleProperty(updateProperties, property, value)
      this.write(updateProperties)
      this.refresh()
    },
    refresh() {
      this.$nextTick(() => {
        this.init()
        this.$emit('sync')
      })
    },
    convertToRules(constraints) {
      if (!constraints) return undefined
      const rules = []
      if (constraints['notEmpty']) { // true
        rules.push({
          message: this.$customTranslate('Must not be empty'), trigger: 'blur', required: true
        })
      }
      if (constraints.minLength && constraints.maxLength) {
        rules.push({
          message: this.$customTranslate('Must between {minLength} and {maxLength}', {
            minLength: constraints.minLength,
            maxLength: constraints.maxLength
          }),
          trigger: 'blur',
          min: constraints.minLength,
          max: constraints.maxLength
        })
      } else if (constraints.minLength) {
        rules.push({
          message: this.$customTranslate('Must have min length {length}', {
            length: constraints.minLength
          }),
          trigger: 'blur',
          min: constraints.minLength
        })
      } else if (constraints.maxLength) {
        rules.push({
          message: this.$customTranslate('Must have max length {length}', {
            length: constraints.maxLength
          }),
          trigger: 'blur',
          max: constraints.maxLength
        })
      }
      let pattern = constraints.pattern, message
      if (pattern) {
        if (typeof pattern !== 'string') {
          message = pattern.message
          pattern = pattern.value
        }
        rules.push({
          message: this.$customTranslate(message) ??
            this.$customTranslate('Must match pattern {pattern}', { pattern: pattern }),
          trigger: 'blur',
          pattern: pattern
        })
      }
      return rules.length === 0 ? undefined : rules
    }
  }
}
</script>
